在上一篇文章中,我們聊到如何用 Context
建立服務,並把服務提供給 Effect
使用。不過在文章的最後我們有提到服務依賴服務的問題。這會帶來一個設計上的挑戰:**怎麼在保持介面乾淨的同時,處理這些依賴?**這一篇我們就是要來講如何使用 Layer
來解決這個問題。
以一個常見的 Web 應用程式為例:
如果我們不小心把依賴直接寫進 Database 服務的介面裡,就會變成這樣:
import { Context, Effect } from "effect"
// Config 與 Logger:僅作為示意
class Config extends Context.Tag("Config")<Config, object>() {}
class Logger extends Context.Tag("Logger")<Logger, object>() {}
// ❌ 錯誤示範:把依賴暴露在介面中
class Database extends Context.Tag("Database")<
Database,
{
readonly query: (
sql: string
) => Effect.Effect<unknown, never, Config | Logger>
}
>() {}
在這個設計中,query
的回傳型別包含了 Config | Logger
。
這代表任何「使用 Database 的程式碼」都必須同時滿足 Config 與 Logger 的需求。
import * as assert from "node:assert"
// 測試替身 Test Double(假資料庫)
const DatabaseTest = Database.of({
query: (_sql) => Effect.succeed([]) // 假裝查詢回傳 []
})
這樣就能單純測試邏輯,例如確認「呼叫 query 會得到陣列」。
const test = Effect.gen(function*() {
const database = yield* Database
const result = yield* database.query("SELECT * FROM users")
assert.deepStrictEqual(result, [])
})
// ┌── Effect<void, never, Config | Logger>
// ▼
const incompleteTestSetup = test.pipe(
Effect.provideService(Database, DatabaseTest)
)
// ┌── ❌ ERROR:Missing 'Config | Logger' in the expected Effect context.
// ▼
Effect.runSync(incompleteTestSetup)
但是因為介面設計把 Config
與 Logger
寫死在型別裡。即便在測試中根本不會用到,TypeScript 也會強迫我們「提供 Config
跟 Logger
」。這就是所謂的 需求外洩 (Requirement Leakage) 問題。
理想狀況下,服務的方法不應再要求任何外部依賴。服務方法的型別應該是:Effect<Success, Error, never>
。所以我們目標就是將依賴在建構階段處理掉,使用者只需要「取得服務」並「呼叫方法」即可。
Layer 會在「建構階段」負責把依賴串起來,以產生我們要的服務。
型別結構如下:
┌─── 產生的服務(RequirementsOut)
│ ┌─── 可能發生的錯誤(Error)
│ │ ┌─── 建構該服務所需的依賴(RequirementsIn)
▼ ▼ ▼
Layer<RequirementsOut, Error, RequirementsIn>
也就是說:Layer 是「如何產生一個服務」的藍圖,其中包含最後會創建什麼服務、建構服務過程中可能發生的錯誤,以及建構該服務所需的依賴。
Layer 名稱 | 依賴 | 型別 |
---|---|---|
ConfigLive | 無 | Layer<Config> |
LoggerLive | 需要 Config | Layer<Logger, never, Config> |
DatabaseLive | 需要 Config 與 Logger | Layer<Database, never, Config | Logger> |
命名慣例:Layer 名稱的 suffix 用 Live
表示正式環境的實作,Test
表示測試用的實作。
ConfigLive
無相依,可以用 Layer.succeed
直接提供一個常數實作:
// 服務定義:提供讀取設定的方法
class Config extends Context.Tag("Config")<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>() {}
// ┌─── Layer<Config, never, never>
// ▼
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:3306/database_name"
})
})
Config
定義「能做什麼」,ConfigLive
則把實作寫好並注入 Effect
的環境。ConfigLive
用 Layer.succeed
建立,因為它沒有依賴、建置不會失敗、也不做 I/O,所以直接用常數把 getConfig
實作好,並註冊為可用的 Config
服務功能。Layer<Config, never, never>
表示:提供 Config
、建置不會失敗、且沒有依賴。Logger 需要 Config
(讀 logLevel
),所以在建構服務時就需要知道 Config 服務的實作。這時候我們就需要用到 Layer.effect
。先從 Effect
的環境把 Config
拿出來,再把 log 的實作「註冊成」 Logger 服務。
Layer.effect(Tag, initEffect)
class Logger extends Context.Tag("Logger")<
Logger,
{
readonly log: (message: string) => Effect.Effect<void>
}
>() {}
// ┌─── Layer<Logger, never, Config>
// ▼
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function*() {
const config = yield* Config
const { logLevel } = yield* config.getConfig
return {
log(message) {
return Effect.sync(() => {
console.log(`[${logLevel}] ${message}`)
})
}
}
})
)
建立 Logger 服務介面跟之前大同小異,我們來講一下 LoggerLive
的實作:
從最後的回傳型別可以看到 RequirementsIn
的部分是 Config
,表示在建構 Logger 服務時,Config 服務是必要的依賴。
我們的 Database 服務需要「對資料庫發出查詢」。它同時需要:
因此它屬於「有依賴、在建置時需要讀取環境」的服務,適合用 Layer.effect
來建置。
class Database extends Context.Tag("Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>() {}
// Layer<Database, never, Config | Logger>
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
return {
query: (sql: string) =>
Effect.gen(function* () {
yield* logger.log(`Executing query: ${sql}`)
const { connection } = yield* config.getConfig
return { result: `Results from ${connection}` }
})
}
})
)
整個建制流程跟 Logger 服務幾乎一樣,區別只在於 query 方法的實作有用到 Logger 與 Config。這也讓 DatabaseLive RequirementsIn
的型別是 Config | Logger
,表示在建構 Database 服務時,Config 與 Logger 服務是必要的依賴。
Layer
是建構服務的藍圖:把依賴解析與初始化放在建置階段完成,對外僅保留 Tag/介面。這樣服務更好測試、更好替換、更好使用。Layer.succeed
(常數)、Layer.effect
(從 Effect 建構)等方式,把每個服務的依賴銜接好。